package com.harry.decoupling.impl.module;

import java.beans.PropertyDescriptor;
import java.util.Arrays;
import java.util.Collection;
import java.util.Deque;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Queue;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.Set;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.collections4.SetUtils;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;

import com.harry.basis.BaseGeneric;
import com.harry.basis.BasePoEntity;
import com.harry.configuration.ConstSettings;
import com.harry.decoupling.defi.module.RestfulMgr;
import com.harry.decoupling.defi.proxy.GenericSrvProxy;
import com.harry.decoupling.defi.tool.DataExchange;
import com.harry.utility.AnnoHelper;
import com.harry.utility.BeanCtxUtils;
import com.harry.utility.InnerUtils;
import com.harry.utility.PoEntityHelper;
import com.harry.utility.excel.ExcelHeader;
import com.harry.utility.excel.ExcelPort;
import com.harry.utility.query.InnerCriteria;
import com.harry.utility.query.InnerCriterion;
import com.harry.utility.query.PageCount;

import lombok.AllArgsConstructor;

public abstract class RestfulMgrImpl<T extends BasePoEntity> extends BaseGeneric<T> implements RestfulMgr<T> {
	@Override
	public void getone(Map<String, Object> params, HttpServletRequest request, HttpServletResponse response, Map<String, Object> data) throws Exception {
		data.put("data", srv().one(getEntityClass(), (String) params.get("id")));
	}

	@Override
	public void getlist(Map<String, Object> params, HttpServletRequest request, HttpServletResponse response, Map<String, Object> data) throws Exception {
		PageCount page = de().parse(params.get("page"), PageCount.class);
		InnerCriteria cri = InnerUtils.ifNull(de().parse(params.get("criteria"), InnerCriteria.class), () -> new InnerCriteria());
		List<T> list = srv().query(getEntityClass(), cri, page);
		data.put("list", list);
		data.put("page", page);
	}

	@Override
	public void post(Map<String, Object> params, HttpServletRequest request, HttpServletResponse response, Map<String, Object> data) throws Exception {
		this.persist(params, request, response, data);
	}

	@Override
	public void patch(Map<String, Object> params, HttpServletRequest request, HttpServletResponse response, Map<String, Object> data) throws Exception {
		if (params.get("id") == null) {
			throw ConstSettings.notFound;
		} else {
			this.persist(params, request, response, data);
		}
	}

	protected void persist(Map<String, Object> params, HttpServletRequest request, HttpServletResponse response, Map<String, Object> data) throws Exception {
		Queue<SafeRecord> queue = new LinkedList<>();
		queue.add(new SafeRecord(getEntityClass(), params));
		Set<Object> handleds = new HashSet<>();
		Deque<SafeRecord> saves = new LinkedList<>();
		// handle circular reference
		Map<Integer, Map<String, Object>> optionalsMap = new LinkedHashMap<>();
		SafeRecord obj;
		while (null != (obj = queue.poll())) {
			Map<String, Object> entity = obj.entity;
			int entityHash = System.identityHashCode(entity);
			if (!handleds.add(entityHash)) {
				continue;
			}
			// make detaches
			Map<String, Object> entityOptionals = InnerUtils.mapObject();
			optionalsMap.put(entityHash, entityOptionals);
			Class<? extends BasePoEntity> entityClass = obj.type;
			saves.addFirst(obj);
			for (String property : PoEntityHelper.keyOf(entity)) {
				PropertyDescriptor pd = BeanUtils.getPropertyDescriptor(entityClass, property);
				// property does not exist, remove it.
				/*-*/if (pd == null) entity.remove(property);
				// isUpdating and unUpdatable
				else if ("id".equals(property));
				// isUpdating and unUpdatable
				else if (!Collection.class.isAssignableFrom(pd.getReadMethod().getReturnType()) && isUpdating(entity) && !PoEntityHelper.isUpdatable(entityClass, property)) entity.remove(property);
				// isInserting and unInsertable
				else if (!Collection.class.isAssignableFrom(pd.getReadMethod().getReturnType()) && isInserting(entity) && !PoEntityHelper.isInsertable(entityClass, property)) entity.remove(property);
				// basic data type, which can be save directly, keep it and skip
				else if (PoEntityHelper.isBasicType(pd.getReadMethod().getReturnType()));
				// null value, which can be save directly, keep it and skip
				else if (entity.get(property) == null);
				// detached value, which should be changed to id-only, and then do the rests
				else if (isDetachedToIdObjAndFalse(obj.type, entity, property));
				else {
					// required value, persist it
					Class<?> fieldType = pd.getReadMethod().getReturnType();
					fieldType = Collection.class.isAssignableFrom(fieldType) ? PoEntityHelper.findComponentType(pd) : fieldType;
					Object[] values = readValueAsArray(obj.type, entity, property);
					boolean persisted = true;
					for (Object value : values) {
						if (!isPersisted(value)) {
							persisted = false;
							queue.add(new SafeRecord(castType(fieldType), InnerUtils.castMap(value)));
						}
					}
					if (!persisted && PoEntityHelper.isNullable(entityClass, property)) {
						entityOptionals.put(property, entity.remove(property));
					}
				}
			}
		}
		for (SafeRecord temp : saves) {
			Map<String, Object> entity = temp.entity;
			Class<? extends BasePoEntity> entityClass = temp.type;
			String id = (String) entity.get("id");
			if (id == null) {
				id = srv().save(de().parse(entity, entityClass)).getId();
			} else if (isExistsMultiPart(temp.type, entity)) {
				srv().save(PoEntityHelper.objectAssign(srv().one(entityClass, id), entity));
			} else if (entity.size() > 1 || (entity.size() > 0 && !entity.containsKey("id"))) {
				Map<String, Object> contains = InnerUtils.mapObject();
				for (Entry<String, Object> entry : entity.entrySet()) {
					PropertyDescriptor pd = BeanUtils.getPropertyDescriptor(entityClass, entry.getKey());
					if (BasePoEntity.class.isAssignableFrom(pd.getReadMethod().getReturnType())) {
						contains.put(entry.getKey() + ".id", PoEntityHelper.readValueByProperty(entry.getValue(), "id"));
					} else {
						contains.put(entry.getKey(), entry.getValue());
					}
				}
				srv().updateByCriteria(entityClass, contains, new InnerCriteria("eq", "id", id));
			}
			entity.clear();
			entity.put("id", id);
		}
		// resume optional
		for (SafeRecord temp : saves) {
			Class<? extends BasePoEntity> entityClass = temp.type;
			Map<String, Object> entityOptionals = optionalsMap.get(System.identityHashCode(temp.entity));
			if (entityOptionals.size() > 0) {
				BasePoEntity entityForUpdate = srv().one(entityClass, (String) temp.entity.get("id"));
				PoEntityHelper.objectAssign(entityForUpdate, entityOptionals);
				srv().save(entityForUpdate);
			}
		}
		data.put("data", srv().one(getEntityClass(), (String) params.get("id")));
	}

	@Override
	public void delete(Map<String, Object> params, HttpServletRequest request, HttpServletResponse response, Map<String, Object> data) throws Exception {
		T root;
		if (params == null || params.get("id") == null || (null == (root = srv().one(getEntityClass(), (String) params.get("id"))))) return;
		else {
			Queue<Object> queue = new LinkedList<>(Arrays.asList(root));
			Set<String> handledStrs = new HashSet<>();
			BasePoEntity obj;
			while (null != (obj = (BasePoEntity) queue.poll())) {
				Class<? extends BasePoEntity> entityClass = obj.getClass();
				// 1. Check if obj has been handled
				if (!handledStrs.add(obj.getClass().getName() + "#" + obj.getId())) {
					continue;
				}
				// 2. Reload obj if it isn't root
				if (handledStrs.size() > 1 && (null == (obj = srv().one(obj.getClass(), obj.getId())))) {
					continue;
				}
				Set<String> properties = PoEntityHelper.getUsefulProperties(entityClass);
				if (isLinkedListAndDetached(entityClass) || isMultiTreeAndDetached(entityClass)) {
					if (linkChildrenToParent(obj) > 0) obj = srv().one(entityClass, obj.getId());
					properties.removeAll(SetUtils.unmodifiableSet("parent", "child", "children"));
				}
				Object value;
				for (String property : properties) {
					Class<?> fieldType = PoEntityHelper.findTypeByProperty(entityClass, property);
					// basic field, skip
					/*-*/if (fieldType == null || PoEntityHelper.isBasicType(fieldType));
					// detached field, skip
					else if (!PoEntityHelper.isCascadable(entityClass, property));
					// null value, skip
					else if ((value = PoEntityHelper.readValueByProperty(obj, property)) == null);
					// multiple field, unbind and -> queue
					else if (PoEntityHelper.isArry(entityClass, property)) {
						queue.addAll(Arrays.asList(PoEntityHelper.toObjectArray(value)));
					}
					// required field, -> queue
					else if (!PoEntityHelper.isNullable(entityClass, property)) {
						queue.add(value);
					}
					// optional field, -> queue
					else {
						queue.add(value);
					}
				}
				deleteWithoutTypeCheck(entityClass, obj);
			}
		}
	}

	@Override
	public void exists(Map<String, Object> params, HttpServletRequest request, HttpServletResponse response, Map<String, Object> data) throws Exception {
		InnerCriteria cri = new InnerCriteria();
		Set<String> uniqueSet = PoEntityHelper.getUsefulProperties(getEntityClass()).stream()// e
				.filter(s -> AnnoHelper.isAnnoable(getEntityClass(), s, "unique"))// e
				.collect(Collectors.toSet());
		List<InnerCriterion> criList = Stream.of(PoEntityHelper.keyOf(params))// e
				.filter(key -> uniqueSet.contains(key))// e
				.map(k -> new InnerCriterion("eq", k, params.get(k)))// e
				.collect(Collectors.toList());
		criList.add(new InnerCriterion("ne", "id", params.get("id")));
		cri.criterions = criList.toArray(ArrayUtils.toArray());
		if (cri.criterions.length > 0) {
			data.put("exists", srv().countByCriteria(getEntityClass(), cri) > 0);
		} else {
			throw new Exception("No unique property offered.");
		}
	}
	// ---------------------------------------------------------------------------------------------------------------//

	@SuppressWarnings("unchecked")
	private Class<? extends BasePoEntity> castType(Object o) {
		return (Class<? extends BasePoEntity>) o;
	}

	@AllArgsConstructor
	private static class SafeRecord {
		private Class<? extends BasePoEntity> type;
		private Map<String, Object> entity;
	}

	@SuppressWarnings("unchecked")
	private void deleteWithoutTypeCheck(Class<?> entityClass, Object obj) throws Exception {
		srv().delete((Class<T>) entityClass, (T) obj);
	}

	private int linkChildrenToParent(BasePoEntity obj) throws Exception {
		// Update ${entityType} set parent.id = ${obj.parent.id} where parent.id = ${obj.id}
		return srv().updateByCriteria(obj.getClass(), InnerUtils.mapObject("parent.id", PoEntityHelper.readValueByProperty(obj, "parent.id")), new InnerCriteria("eq", "parent.id", obj.getId()));
	}

	private Object[] readValueAsArray(Class<? extends BasePoEntity> type, Map<String, Object> entity, String property) {
		PropertyDescriptor pd = BeanUtils.getPropertyDescriptor(type, property);
		Object[] values;
		if (Collection.class.isAssignableFrom(pd.getReadMethod().getReturnType())) {
			values = PoEntityHelper.toObjectArray(PoEntityHelper.readValueByProperty(entity, property));
		} else {
			values = ArrayUtils.toArray(PoEntityHelper.readValueByProperty(entity, property));
		}
		return values;
	}

	private boolean isMultiTreeAndDetached(Class<? extends BasePoEntity> entityClass) {
		Set<String> properties = PoEntityHelper.getUsefulProperties(entityClass);
		return properties.containsAll(SetUtils.unmodifiableSet("parent", "children")) && !PoEntityHelper.isCascadable(entityClass, "children");
	}

	private boolean isLinkedListAndDetached(Class<? extends BasePoEntity> entityClass) {
		Set<String> properties = PoEntityHelper.getUsefulProperties(entityClass);
		return properties.containsAll(SetUtils.unmodifiableSet("parent", "child")) && !PoEntityHelper.isCascadable(entityClass, "child");
	}

	private boolean isDetachedToIdObjAndFalse(Class<? extends BasePoEntity> type, Map<String, Object> params, String property) {
		boolean cascadable = PoEntityHelper.isCascadable(type, property);
		Object value = PoEntityHelper.readValueByProperty(params, property);
		if (PoEntityHelper.isArry(type, property)) {
			Object[] objects = PoEntityHelper.toObjectArray(value);
			for (int i = 0; i < objects.length; i++) {
				boolean isInsert = PoEntityHelper.readValueByProperty(objects[i], "id") == null;
				if (!cascadable) {
					objects[i] = InnerUtils.idObj(objects[i]);
				} else if (isInsert && !PoEntityHelper.isInsertable(type, property)) {
					objects[i] = null;
				} else if (!isInsert && !PoEntityHelper.isUpdatable(type, property)) {
					objects[i] = InnerUtils.idObj(objects[i]);
				}
			}
			params.put(property, objects);
		} else if (!cascadable) {
			params.put(property, InnerUtils.idObj(value));
		}
		return false;
	}

	private boolean isExistsMultiPart(Class<? extends BasePoEntity> type, Map<String, Object> patchs) {
		return patchs.keySet().stream().anyMatch(property -> Collection.class.isAssignableFrom(BeanUtils.getPropertyDescriptor(type, property).getReadMethod().getReturnType()));
	}

	private boolean isPersisted(Object value) throws Exception {
		return value == null || (PoEntityHelper.readValueByProperty(value, "id") != null && Map.class.cast(value).keySet().size() == 1);
	}

	private boolean isInserting(Map<String, Object> params) {
		return params.get("id") == null;
	}

	private boolean isUpdating(Map<String, Object> params) {
		return params.get("id") != null;
	}

	// ------------------------ excel ---------------------------------
	@SuppressWarnings("unchecked")
	@Override
	public void getexcel(Map<String, Object> params, HttpServletRequest request, HttpServletResponse response, Map<String, Object> data) throws Exception {
		Map<String, Object> criteria;
		List<Object> projections;
		if (false || // e
				(criteria = de().parseMap(params.get("criteria"))) == null || // e
				(projections = de().parse(criteria.get("projections"), LinkedList.class)) == null) {
			throw new Exception("criteria.projections does not exists. ");
		} else if (projections.size() < 2) {
			throw new Exception("criteria.projections ");
		} else {
			getlist(params, request, response, data);
			List<Object> list = new LinkedList<>(de().parseCollection(data.get("list")));
			List<Map<String, Object>> ds = new LinkedList<>();
			for (int i = 0; i < list.size(); i++) {
				ds.add(create_ds_item(de().parse(PoEntityHelper.readValueByProperty(list.get(i), "values"), LinkedList.class), projections));
			}
			Collection<ExcelHeader> hs = new LinkedList<>();
			for (int i = 0; i < projections.size(); i++) {
				hs.add(create_hs_item(de().parseMap(projections.get(i))));
			}
			InnerUtils.setFileName(response, getEntityClass().getSimpleName() + ".xlsx");
			new ExcelPort(hs, ds).out(response.getOutputStream());
		}
	}

	private ExcelHeader create_hs_item(Map<String, Object> projection) {
		String property = (String) projection.get("property");
		String fieldName = fetchFeildName(property);
		String fieldTitle = fetchCommentByField(getEntityClass(), property);
		return new ExcelHeader(fieldTitle, fieldName);
	};

	private Map<String, Object> create_ds_item(List<?> values, List<?> projections) throws Exception {
		Map<String, Object> ds_item = new LinkedHashMap<>();
		if (values.size() != projections.size()) {
			throw new Exception("Impossible Exception that projections.length != values.length.");
		} else {
			for (int i = 0; i < projections.size(); i++) {
				Map<String, Object> projection = de().parseMap(projections.get(i));
				String property = (String) projection.get("property");
				String fieldName = fetchFeildName(property);
				ds_item.put(fieldName, values.get(i));
			}
		}
		return ds_item;
	}

	private String fetchFeildName(String property) {
		return property.substring(property.lastIndexOf(".") + 1, property.length());
	}

	private String fetchCommentByField(Class<T> entityClass, String property) {
		return StringUtils.join((property.substring(0, 1).toUpperCase() + property.substring(1)).split("\\."), ' ');
	}

	// ------------------ statics ------------------------------------
	protected static GenericSrvProxy srv() {
		return BeanCtxUtils.getBeanByType(GenericSrvProxy.class);
	}

	protected static DataExchange de() {
		return BeanCtxUtils.getBeanByType(DataExchange.class);
	}
}
